/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package image.edge;

import image.filter.GaussianFilter;
import image.processing.ImageProcessor;
import image.processing.ImageProcessor.CompressAlgorithm;
import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import mask.ImageMask;

/**
 *
 * @author tommy
 */
public class CannyEdgeDetector extends EdgeDetectorImpl {
	private ImageMask horzDerivativeMask, vertDerivativeMask;
	private static double edgeThreshold = 5d;
	private double maxGradient;
	private double t1, t2;
	
	public CannyEdgeDetector(BufferedImage src, double sigma, double t1, double t2) {
		// applies gaussian filter to smooth image and eliminate noise
		super(new GaussianFilter(sigma*sigma).applyFilter(src, CompressAlgorithm.LINEAR));
		double[][] horzSobelOperator = {
		  { -1d, 0d, 1d },
		  { -2d, 0d, 2d },
		  { -1d, 0d, 1d },
		};
		
		double[][] vertSobelOperator = {
			{1d, 2d, 1d},
			{ 0d, 0d, 0d },
			{ -1d, -2d, -1d }
		};
		this.horzDerivativeMask = new ImageMask(horzSobelOperator, 3);
		this.vertDerivativeMask = new ImageMask(vertSobelOperator, 3);
		maxGradient = Double.MIN_VALUE;
		this.t1 = t1;
		this.t2 = t2;
	}
	
	public BufferedImage FindEdges() {
		Double[][] matGradMagnitudes;
		Double[] gradientMagnitudes;
		Double[] gdx, gdy;
		GradientAngle[] gdir;

		gradientMagnitudes = new Double[ret.getHeight()*ret.getWidth()];
		
		// Applies Sobel Operator to find gradient levels in X and Y directions
		// for each intensity pixel!
		// does not process padded border edge
		gdx = horzDerivativeMask.applyMask(src);
		gdy = vertDerivativeMask.applyMask(src);

		// gets gradient intensity by pixel basis
		for (int i = 0 ; i < gradientMagnitudes.length ; i++) {
			gradientMagnitudes[i] = Math.sqrt(gdx[i]*gdx[i] + gdy[i]*gdy[i]);
			
			if (gradientMagnitudes[i] > maxGradient) {
				maxGradient = gradientMagnitudes[i];
			}
		}
		
		// normalizes gradientMagntudes to [0-1]
		normalize(gradientMagnitudes);
		// gets ortogonal direction of edge for each pixel that is an edge
		gdir = getGradientsDirections(gdx, gdy);
		/*String angle;
		int i = 0;
		for (GradientAngle g : gdir) {
			if (g == GradientAngle.Zero) {
				angle = "ZERO";
			} else if (g == GradientAngle.HalfPI) {
				angle = "HALF-PI";
			} else if (g == GradientAngle.QuarterPI) {
				angle = "QUARTER-PI";
			} else {
				angle = "THREEQUATER-PI";
			}
			System.out.println("gdx: " + gdx[i++] + " angle: " + angle);
		}*/

		// performs non-maximun suppression
		NonMaximunSuppresion(gradientMagnitudes, gdir);
		WritableRaster raster = ret.getRaster();
		
		/*for (int i = 0 ; i < ret.getHeight() * ret.getWidth(); i++) {
			raster.setSample(i % ret.getWidth(), i / ret.getWidth(), 0, gradientMagnitudes[i] > 0 ? 255 : 0);
		}*/
		
		
		
		// performs hysteresis threshold
		hysteresisThreshold(gradientMagnitudes, t1, t2);
		return ret;
	}
	
	private void normalize(Double[] gMagnitudes) {
		for (int i = 0 ; i < gMagnitudes.length ; i++) {
			gMagnitudes[i] /= maxGradient;
		}
		return;
	}
	
	/*
	 * Obtains ortogonal direction of edge pixel, this is done by previously
	 * estimating pixel gradient (in both horz and vert direction) and
	 * doing tan^-1(gdy/gdx) and constraining to 4 different angles: 0, 45, 90, 135
	 * because edge direction in an image can be either horizontal, vertical or diagonal
	 */
	
	private GradientAngle[] getGradientsDirections(Double[] gdx, Double[] gdy) {
		GradientAngle[] directions = new GradientAngle[gdx.length];
		double angle;
		
		
		for (int i = 0 ; i < gdx.length ; i++) {
			if (gdx[i] != 0) {
				angle = Math.atan2(gdy[i],gdx[i]);
			} else {
				if (gdy[i] != 0) {
					angle = Math.PI/2;
				} else {
					angle = 0;
				}
			}
			
			if (angle < 0d) {
				angle += Math.PI; // vertical quadrant symetry
			}
			
			directions[i] = constraintAngle(angle);
		}
		
		return directions;
	}
	
	// performs angle constraining
	private GradientAngle constraintAngle(double angle) {
		GradientAngle retAngle;
		
		// Reassignment by quadrants
		if (angle <= (Math.PI/8d)) {
			retAngle = GradientAngle.Zero; // East/West
		} else if (angle <= (3*Math.PI / 8d)) {
			retAngle = GradientAngle.QuarterPI; // Northeast / Southwest
		} else if (angle <= (5*Math.PI / 8d)) {
			retAngle = GradientAngle.HalfPI; // North/South
		} else if (angle <= (7*Math.PI / 8d)) {
			retAngle = GradientAngle.ThreeQuarterPI; // Northwest / Southeast
		} else {
			retAngle = GradientAngle.Zero; // East/West (West part)		
		}
		//System.out.println(retAngle);
		return retAngle;
	}
	
	private void NonMaximunSuppresion(Double[] gradientMagnitudes, GradientAngle[] directions) {
		//Double[][] matGradientMagnitudes; // gradMagnitudes as a matrix
		double gmagnitude, n1, n2; // n1,n2 = neighbours in ortogonal edge direction
		
		// transforms to matrix for easy neighbour manipulation in non-maximun suppresion
		//matGradientMagnitudes = ImageProcessor.toMatrix(gradientMagnitudes, ret.getHeight(), ret.getWidth());
		//matGradientMagnitudes = new Double[ret.getHeight()][ret.getWidth()];
		
		for (int i = 0 ; i < gradientMagnitudes.length ; i++) {
			// check if index points to image borders
			if (i < ret.getWidth() || i >= gradientMagnitudes.length - ret.getWidth() || 
				i % ret.getWidth() == 0 || i % ret.getWidth() == ret.getWidth() - 1) {
				gradientMagnitudes[i] = 0d;
			} else {
				gmagnitude = gradientMagnitudes[i];
				
				if (gmagnitude > 0) {
					switch(directions[i]) {
						case Zero: 
							n1 = gradientMagnitudes[i-1];
							n2 = gradientMagnitudes[i+1];
							break;
						case QuarterPI:
							n1 = gradientMagnitudes[i - ret.getWidth() + 1];
							n2 = gradientMagnitudes[i + ret.getWidth() - 1];
							break;
						case HalfPI:
							n1 = gradientMagnitudes[i - ret.getWidth()];
							n2 = gradientMagnitudes[i + ret.getWidth()];
							break;
						case ThreeQuarterPI:
						default:
							n1 = gradientMagnitudes[i + ret.getWidth() + 1];
							n2 = gradientMagnitudes[i - ret.getWidth() - 1];
					}

					// suppress!
					if (gmagnitude <= n1 || gmagnitude <= n2) {
						gradientMagnitudes[i] = 0d;
					}
				}
			} 
		}
		
		/*for (int y = 0 ; y < ret.getHeight() ; y++) {
			for (int x = 0 ; x < ret.getWidth() ; x++) {
				gmagnitude = matGradientMagnitudes[y][x];

				try {
					// if gmagnitude is a possible edge, analyze and suppress when necessary
					if (gmagnitude > 0) {
						switch(directions[y*ret.getWidth() + x]) {
							case Zero: 
								if (gmagnitude < matGradientMagnitudes[y][x-1] || gmagnitude < matGradientMagnitudes[y][x+1]) {
									matGradientMagnitudes[y][x] = 0d;
								}
								break;
							case QuarterPI:
								if (gmagnitude < matGradientMagnitudes[y-1][x+1] || gmagnitude < matGradientMagnitudes[y+1][x-1]) {
									matGradientMagnitudes[y][x] = 0d;
								}
								break;
							case HalfPI:
								if (gmagnitude < matGradientMagnitudes[y-1][x] || gmagnitude < matGradientMagnitudes[y+1][x]) {
									matGradientMagnitudes[y][x] = 0d;
								}
								break;
							case ThreeQuarterPI:
							default:
								if (gmagnitude < matGradientMagnitudes[y+1][x+1] || gmagnitude < matGradientMagnitudes[y-1][x-1]) {
									matGradientMagnitudes[y][x] = 0d;
								}
						}
					} else {
						matGradientMagnitudes[y][x] = 0d;
					}
				} catch (Exception e) {
					// illegal neighbour. We're on image contour, so gradient goes to 0
					matGradientMagnitudes[y][x] = 0d;
				}
			}
		}
		
		return matGradientMagnitudes; 
		 */
	}
	
	// performs hysteresis threshold and 8-conex connection check. t1 and t2 are
	// the low and high threshold respectively
	private void hysteresisThreshold(Double[] gMagnitudes, double t1, double t2) {
		WritableRaster outRaster;
		int w; //img width
		outRaster = ret.getRaster();
		
		w = ret.getWidth();
		
		for (int i = 0 ; i < gMagnitudes.length ; i++) {
			if (gMagnitudes[i] <= t1) {
				gMagnitudes[i] = 0d;
			}
		}
		
		// for each genuine edge pixel, trace it and update edge image output
		// and if it's a weak edge, discard it as possible edge
		for (int i = 0 ; i < gMagnitudes.length ; i++) {
			if (outRaster.getSample(i % w, i / w, 0) == 0 && gMagnitudes[i] > t2) {
				traceEdge(outRaster, gMagnitudes, i);
			}
		}
	}
	
	// idx: flat index of array
	private void traceEdge(WritableRaster raster, Double[] gMagnitudes, int idx) {
		int w = ret.getWidth();
		int x,y, windowIdx; // windowIdx: index of gMagnitudes of neighbours around pixel idx
		
		x = idx % w;
		y = idx / w;
		
		if (raster.getSample(x, y, 0) == 0 && gMagnitudes[idx] > t1) {
			raster.setSample(x, y, 0, 255);
		}
		
		try {

			// avoids center pixel
			
			windowIdx = (y-1)* w + x; // north
			if (raster.getSample(x, y-1, 0) == 0 &&
				gMagnitudes[windowIdx] > t1) {

				traceEdge(raster, gMagnitudes, windowIdx);
			}
			
			windowIdx = (y)* w + x-1; // west
			if (raster.getSample(x-1, y, 0) == 0 &&
				gMagnitudes[windowIdx] > t1) {

				traceEdge(raster, gMagnitudes, windowIdx);
			}
			
			windowIdx = (y)* w + x+1; // east
			if (raster.getSample(x+1, y, 0) == 0 &&
				gMagnitudes[windowIdx] > t1) {

				traceEdge(raster, gMagnitudes, windowIdx);
			}
			
			windowIdx = (y+1)* w + x; // south
			if (raster.getSample(x, y+1, 0) == 0 &&
				gMagnitudes[windowIdx] > t1) {

				traceEdge(raster, gMagnitudes, windowIdx);
			}

		} catch (Exception e) {
			// do nothing, coord was out of bounds
		}
		
	}
}
